﻿/* 
  Copyright (c) SilverLining 2015
  Date:    2016-1-7
  File:    GameClient.cs  
  Description: 
    Net client implementation.
*/

using System;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Sockets;
using System.Net.Security;
using System.Linq;
using System.Security.Authentication;

/// <summary>
/// The net namespace contains the client-server code for the game engine.
/// </summary>
namespace SL.M.Net
{
    using Debug;

    /// <summary>
    /// Represent a multiplayer game server.
    /// </summary>
    /// <remarks>After connecting, the following message types are made available.
    /// <para>A <code>us.sl.mgame.serverFrame</code> timer message is fired after the client and server have exchanged frames.</para>
    /// <para>A <code>us.sl.mgame.clientAccepted</code> message is fired when the server accepts the client connection.</para>
    /// <para>Users can determine when the server has stopped accepting connections by checking the <code>us.sl.m.stop</code> event.</para>
    /// <see cref="SL.M.Net.GameServerSession"/>
    /// <see cref="SL.M.GameObject"/>
    /// </remarks>
    public class GameClient<T> : Process where T : GameObject
    {
        const string Tag = "client: ";

        string _hostName;
        int _port;
        ConnectionManager _manager;
        TcpClient _server;
        ServerFrame<T> _frame = new ServerFrame<T>();
        Func<GameObject> _authorizationInitializer;
        SessionFlags _flags;
        string _terminationMessage;
        bool _disposed = false;
        bool _faulted = false;

        /// <summary>
        /// Return the fault message for this client, if it has been terminated.
        /// </summary>
        public string FaultMessage
        {
            get
            {
                return _terminationMessage;
            }
        }

        /// <summary>
        /// Return if the client has faulted.
        /// </summary>
        public bool IsFaulted
        {
            get
            {
                return _faulted;
            }
        }

        /// <summary>
        /// Return or set the server read timeout.
        /// </summary>
        public TimeSpan ReadTimeout
        {
            get;
            set;
        }

        /// <summary>
        /// Return or set the server write timeout.
        /// </summary>
        public TimeSpan WriteTimeout
        {
            get;
            set;
        }

        /// <summary>
        /// Return or set the server read buffer size.
        /// </summary>
        public int ReadBufferSize
        {
            get;
            set;
        }

        /// <summary>
        /// Return the server object being used for this client session.
        /// </summary>
        public T ServerObject
        {
            get
            {
                return _frame.ServerObject;
            }
        }

        /// <summary>
        /// Return the session id of this client session.
        /// </summary>
        public int SessionId
        {
            get
            {
                return _frame.SessionId;
            }
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="hostName">The host to connect to.</param>
        /// <param name="port">The port to connect to.</param>
        /// <param name="serverState">A reference to the server state object to be used for receiving server frames.</param>
        /// <param name="authorizationInitializer">A functor which initializes the authorization state to be used during the authorization phase for a specified client. The client should send the correct state to authorize itself with the server. If the authorization action fails by throwing an exception, the message will be returned to the client.</param>
        /// <param name="flags">The session flags, if any.</param>
        public GameClient(string hostName, int port, T serverState, Func<GameObject> authorizationInitializer, SessionFlags flags)
        {
            if (String.IsNullOrEmpty(hostName))
            {
                throw new ArgumentException("Argument cannot be null or empty.", "hostName");
            }

            if (serverState == null)
            {
                throw new ArgumentNullException("serverState");
            }

            if (authorizationInitializer == null)
            {
                throw new ArgumentNullException("authorizationInitializer");
            }

            if (System.Diagnostics.Debugger.IsAttached)
            {
                ReadTimeout = WriteTimeout = TimeSpan.MaxValue;
            }
            else
            {
                ReadTimeout = Properties.Settings.Default.ReadTimeout;
                WriteTimeout = Properties.Settings.Default.WriteTimeout;
            }
            ReadBufferSize = Properties.Settings.Default.ReadBufferSize;
            _hostName = hostName;
            _port = port;
            _frame.ServerObject = serverState;
            _authorizationInitializer = authorizationInitializer;
            _flags = flags;
        }

        /// <summary>
        /// Process entry point.
        /// </summary>
        protected override void ProcessEntry()
        {
            DebugProvider.Log(DebugProvider.LastHr, Tag + "Starting GameClient");

            try
            {
                WrapNetAction(DoClientSession);
            }
            finally
            {
                if (_server != null)
                {
                    _server.Close();
                }
            }
        }

        /// <summary>
        /// Dispose of this object.
        /// </summary>
        /// <param name="disposing">Whether or not we are being called from a disposer.</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_frame")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1725:ParameterNamesShouldMatchBaseDeclaration", MessageId = "0#")]
        protected override void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                _disposed = true;

                if (disposing)
                {
                    if (_manager != null)
                    {
                        _manager.Dispose();
                        _manager = null;
                    }

                    if (_server != null)
                    {
                        _server.Close();
                        _server = null;
                    }
                }
            }
            base.Dispose(disposing);
        }


        void DoClientSession()
        {
            DebugProvider.Log(DebugProvider.LastHr, Tag + "Target: " + _hostName + ":" + _port);
            IPAddress address = Dns.GetHostAddresses(_hostName).FirstOrDefault();
            IPEndPoint ipEp = new IPEndPoint(address, _port);
            DebugProvider.Log(DebugProvider.LastHr, Tag + "Client connecting to: {0}", ipEp);

            // Set up the stream. We have to finish negotiation before we can really do anything else.
            _server = new TcpClient(address.AddressFamily);
            _server.ReceiveTimeout = (int)ReadTimeout.TotalMilliseconds;
            _server.SendTimeout = (int)WriteTimeout.TotalMilliseconds;
            _server.Connect(ipEp);

            _manager = new ConnectionManager(_hostName, _server, _flags, false);

            GameObject authorizationObject = _authorizationInitializer();
            _manager.SendFrame(authorizationObject);
            RecieveFrame();

            // TODO: add another channel for the client to communicate with the server on.

            Send(GameServerSession<T>.ClientAcceptedType);

            while (!_faulted && Started)
            {
                while (NextMessage()) ;
                RecieveFrame();
                Send(GameServer<T>.ServerFrameType);
            }
        }

        void WrapNetAction(Action netAction)
        {
            Exception thrown = null;
            string faultMessage = null;

            try
            {
                netAction();
            }
            catch (SocketException se)
            {
                faultMessage = se.Message;
                thrown = se;
            }
            catch (IOException ie)
            {
                faultMessage = ie.Message;
                thrown = ie;
            }
            catch (SessionAbortedException sa)
            {
                faultMessage = sa.Message;
                thrown = sa;
            }
            catch (BadResultException br)
            {
                faultMessage = "Data corrupt";
                thrown = br;
            }
            catch (AuthenticationException ae)
            {
                faultMessage = "SSL negotiation failed";
                thrown = ae;
            }
            finally
            {
                if (thrown != null)
                {
                    DebugProvider.Log(DebugProvider.LastHr, Tag + "Client connection failed. Actual exception: " + thrown.ToString());
                    _faulted = true;
                    if (String.IsNullOrEmpty(_terminationMessage))
                    {
                        _terminationMessage = faultMessage;
                    }
                }
            }
        }
        
        void RecieveFrame()
        {
            if (!_faulted && Started)
            {
                _manager.RecieveFrame(_frame);
                if (_frame.Faulted)
                {
                    throw new SessionAbortedException("Server closed the connection: " + _frame.Message ?? "no reason");
                }
            }
        }
    }
}
